<?php
declare(strict_types=1);

namespace app\shop\library\excel;

use ba\Filesystem;
use Exception;
use Generator;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Alignment;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PhpZip\ZipFile;
use Throwable;

class PhpSpreadSheet
{
    private Spreadsheet $spreadsheet;

    private Worksheet $sheet;

    /**
     * 表头信息
     * @var array
     */
    private array $exportHeader = [];

    /**
     * 导出的文件名
     * @var string
     */
    private string $exportFileName = '';

    /**
     * 导出文件后缀
     * @var string
     */
    private string $exportFileSuffix = '.xlsx';

    /**
     * 导出文件保存到临时目录
     * @var bool
     */
    private bool $saveExportFileToTemp = false;

    /**
     * 导出到临时目录的文件编号
     * @var int
     */
    private int $tempFileIndex = 1;

    /**
     * 导出时每个表可写入的最大数据行
     * @var int
     */
    private int $exportMaxRow = 10000;

    /**
     * 导出时写入表格的数据行
     * @var int
     */
    private int $exportDataRow = 0;

    public function __construct()
    {
        set_time_limit(0);

        $this->spreadsheet = new Spreadsheet();

        $this->sheet = $this->spreadsheet->getActiveSheet();
    }

    /**
     * 设置单个Excel文件最大保存条数
     * @param int $row
     * @return $this
     */
    public function setExportMaxRow(int $row): static
    {
        $this->exportMaxRow = $row;

        return $this;
    }

    /**
     * 设置导出表头
     * @param array $header
     * <br> column 字段名
     * <br> name 字段标题
     * <br> width 列度
     * <br> height 行高
     * <br> color 字体颜色
     * <br> type 字段类型
     * @return $this
     */
    public function setExportHeader(array $header): static
    {
        $this->exportHeader = $header;

        $rowNumber = 1;

        $cellName = $this->getCellName();

        $defaultColor = 'FF000000';
        foreach ($header as $key => $val) {
            $column = $cellName[$key] . $rowNumber;
            $this->sheet->setCellValue($column, ' ' . $val['name']);
            $this->sheet->getStyle($column)->getFont()->setBold(true);
            // 设置宽度
            $width = $val['width'] ?? 'auto';
            if ($width === 'auto') {
                // 自动换行
                $this->sheet->getStyle($cellName[$key])->getAlignment()->setWrapText(true);
                // 自动宽度
                $this->sheet->getColumnDimension($cellName[$key])->setAutoSize(true);
            } else {
                $this->sheet->getColumnDimension($cellName[$key])->setWidth((float)$val['width']);
            }
            //设置高度
            $height = $val['height'] ?? 'auto';
            if ($height === 'auto') {
                $this->sheet->getRowDimension($rowNumber)->setRowHeight(-1);
            } else {
                $this->sheet->getRowDimension($rowNumber)->setRowHeight((float)$val['height']);
            }
            // 设置字体颜色
            $color = ($val['color'] ?? '') ?: $defaultColor;
            $this->sheet->getStyle($column)->getFont()->getColor()->setARGB($color);
            // 设置文本居中
            $this->sheet->getStyle($column)
                ->getAlignment()
                ->setHorizontal(Alignment::VERTICAL_CENTER)
                ->setVertical(Alignment::VERTICAL_CENTER);
            // 设置货币
            if (isset($val['format']) && $val['format'] == 'currency') {
                $this->sheet->getStyle($cellName[$key])->getNumberFormat()->setFormatCode('"¥"#,##0.00');
            }
        }

        $this->exportDataRow += 1;

        return $this;
    }

    /**
     * 设置导出文件名
     * @param string $name
     * @return $this
     */
    public function setExportFileName(string $name): static
    {
        $this->exportFileName = $name;

        $this->sheet->setTitle($name);

        return $this;
    }

    /**
     * 设置导出数据
     * @param array $data
     * @return PhpSpreadSheet
     * @throws Exception
     */
    public function setExportData(array $data): static
    {
        if (empty($this->exportHeader)) {
            throw new Exception('缺少导出表头信息');
        }

        $data      = iterator_to_array($this->yieldData($data));
        $dataCount = count($data);

        if ($dataCount >= $this->exportMaxRow) {
            // 一次性写入数据超过最大行数,分割数据
            $this->processLargeData($data);
        } else {
            $cumulative = $dataCount + $this->exportDataRow;

            // 累计超过最大行数
            if ($cumulative >= $this->exportMaxRow) {
                // 当前表格还能写入的数量
                $spliceRow = $this->exportMaxRow - $this->exportDataRow;
                $nowData   = array_splice($data, 0, $spliceRow);
                $this->sheet->fromArray($nowData, null, 'A' . $this->sheet->getHighestRow() + 1);
                $this->saveToTempFile();
                $this->initNewSpreadSheet();
                if ($data) {
                    $this->sheet->fromArray($data, null, 'A' . $this->sheet->getHighestRow() + 1);
                    $this->exportDataRow += count($data);
                }
            } else {
                // 未超过最大行数
                $this->sheet->fromArray($data, null, 'A' . $this->sheet->getHighestRow() + 1);
                $this->exportDataRow += count($data);
                // 存在临时文件
                if (empty($data) && $this->saveExportFileToTemp) {
                    // 写入临时文件
                    $this->saveToTempFile();
                }
            }
        }


        return $this;
    }

    /**
     * 分割数据
     * @param array $data
     * @return void
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    private function processLargeData(array $data): void
    {
        while ($data) {
            $nowData = array_splice($data, 0, $this->exportMaxRow - $this->exportDataRow);
            $this->sheet->fromArray($nowData, null, 'A' . $this->sheet->getHighestRow() + 1);
            $this->saveToTempFile();
            if (count($data) > 0) {
                $this->initNewSpreadSheet();
            }
        }
    }

    private function yieldData($data): Generator
    {
        foreach ($data as $v) {
            yield $v;
        }
    }

    private function getCellName(): array
    {
        $cellName = [];
        $letters  = range('A', 'Z');
        foreach ($letters as $letter) {
            $cellName[] = $letter;
        }
        for ($i = 0; $i < count($letters); $i++) {
            foreach ($letters as $letter) {
                $cellName[] = $letters[$i] . $letter;
            }
        }
        return $cellName;
    }

    /**
     * 导出到服务器
     * @return string
     * <br>save_path 文件相对路径
     * @throws Exception
     */
    public function exportToServer(): string
    {
        // 导出压缩包
        if ($this->saveExportFileToTemp) {
            // 判断下当前是否有已写入但未保存的临时文件
            $currentRowNumber = $this->sheet->getHighestRow() - 1;
            if ($currentRowNumber > 0) {
                $this->saveToTempFile();
            }

            return $this->createZipFile();
        }

        $relativePath = $this->getSaveDir() . $this->exportFileName . $this->exportFileSuffix;
        $absolutePath = $this->getSaveDir(true) . $this->exportFileName . $this->exportFileSuffix;

        try {
            $suffix    = trim($this->exportFileSuffix, '.');
            $objWriter = IOFactory::createWriter($this->spreadsheet, ucfirst($suffix));
            $objWriter->save($absolutePath);
        } catch (Throwable $e) {
            throw new Exception($e->getMessage());
        } finally {
            //释放内存
            $this->spreadsheet->disconnectWorksheets();
            unset($this->spreadsheet);
        }

        return $relativePath;
    }

    /**
     * 创建ZIP压缩文件
     * @return string 返回ZIP文件的路径
     * @throws Exception
     */
    public function createZipFile(): string
    {
        $zipFileName = $this->exportFileName . ".zip";

        $relativePath = $this->getSaveDir() . $zipFileName;
        $absolutePath = $this->getSaveDir(true) . $zipFileName;

        $zip = new ZipFile();
        try {
            $suffix = trim($this->exportFileSuffix, '.');
            $files  = Filesystem::getDirFiles($this->getTempFileSaveDir(true), [$suffix]);
            foreach ($files as $file) {
                $zip->addFile($file);
            }
            $zip->saveAsFile($absolutePath);
        } catch (Throwable $e) {
            throw new Exception("无法创建ZIP文件:" . $e->getMessage());
        } finally {
            Filesystem::delDir($this->getTempFileSaveDir(true));
            $zip->close();
        }

        return $relativePath;
    }

    /**
     * 获取导出文件保存目录
     * @param bool $fullPath
     * @return string
     */
    public function getSaveDir(bool $fullPath = false): string
    {
        $path = '/storage/export/' . date('Y-m-d') . '/';

        if (!is_dir(public_path() . $path)) {
            mkdir(public_path() . $path, 0755, true);
        }

        return $fullPath ? public_path() . $path : $path;
    }

    /**
     * 获取临时文件保存目录
     * @param bool $fullPath
     * @return string
     */
    private function getTempFileSaveDir(bool $fullPath = false): string
    {
        return $this->getSaveDir($fullPath) . $this->exportFileName . '_tmp/';
    }

    /**
     * 初始化新的表格
     * @return void
     */
    private function initNewSpreadSheet(): void
    {
        $this->exportDataRow = 0;

        // 添加表头和sheet名
        $this->setExportHeader($this->exportHeader);
        $this->sheet->setTitle($this->exportFileName);
    }

    /**
     * 保存临时文件
     * @return void
     * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
     */
    private function saveToTempFile(): void
    {
        // 清理旧的缓存文件
        if (!$this->saveExportFileToTemp) {
            Filesystem::delDir($this->getTempFileSaveDir(true));
        }

        $suffix    = trim($this->exportFileSuffix, '.');
        $objWriter = IOFactory::createWriter($this->spreadsheet, ucfirst($suffix));

        $savePath = $this->getTempFileSaveDir(true);
        if (!is_dir($savePath)) {
            mkdir($savePath, 0755, true);
        }

        $savePath .= $this->exportFileName . "($this->tempFileIndex)" . $this->exportFileSuffix;
        $objWriter->save($savePath);

        $this->saveExportFileToTemp = true;
        $this->tempFileIndex++;

        // 创建新的表格
        $this->spreadsheet->disconnectWorksheets();
        unset($this->spreadsheet);

        $this->spreadsheet = new Spreadsheet();
        $this->sheet       = $this->spreadsheet->getActiveSheet();
    }
}